/*
 * The Yard Utilties - http://www.theyard.net/
 * 
 * Copyright (c) 2008 by Vlideshow, Inc..  All Rights Resrved.
 * 
 * This library is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License as published by the Free Software 
 * Foundation; either version 2.1 of the License, or (at your option) any later 
 * version. 
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY 
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along 
 * with this library; if not, write to the Free Software Foundation, Inc., 
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 */
package net.theyard.components.netstreamsm
{
  import net.theyard.components.netconnectionsm.NetConnectionStateMachine;
  import net.theyard.components.netconnectionsm.NetStatus;
  import net.theyard.components.netstreamsm.events.CuePointEvent;
  import net.theyard.components.netstreamsm.events.MetaDataEvent;
  import net.theyard.components.sm.StateMachine;
  import net.theyard.components.sm.events.StateChangeEvent;
  import net.theyard.components.Utils;

  import flash.events.AsyncErrorEvent;
  import flash.events.IOErrorEvent;
  import flash.events.NetStatusEvent;
  import flash.media.Camera;
  import flash.media.Microphone;
  import flash.media.SoundTransform;
  import flash.media.Video;
  import flash.net.NetConnection;
  import flash.net.NetStream;
  import flash.utils.Dictionary;

  /**
   * This is a state machine wrapper for the flash.net.NetStream class. 
   *
   * <p>
   * It's goal is to abstract away all the NetStatus events and
   * instead represent the state of the NetStream in simple, easy
   * to understand concepts.
   * </p><p>
   * They are:
   * <ul>
   * <li>
   * <a href="#STATE_STOPPED">STATE_STOPPED</a>: The stream is currently stopped.
   * </li><li>
   * <a href="#STATE_STARTPLAY">STATE_STARTPLAY</a>: The stream is attempting to play.
   * </li><li>
   * <a href="#STATE_PLAYING">STATE_PLAYING</a>: The stream is currently playing.
   * </li><li>
   * <a href="#STATE_STOPPING">STATE_STOPPING</a>: The stream is either stopping playing or stopping publishing
   * </li><li>
   * <a href="#STATE_STARTPUBLISH">STATE_STARTPUBLISH</a>: The stream is attempting to publish.
   * </li><li>
   * <a href="#STATE_PUBLISHING">STATE_PUBLISHING</a>: The stream is currently publishing
   * </li><li>
   * <a href="#STATE_PAUSE">STATE_PAUSE</a>: The stream is currently paused while playing
   * </li><li>
   * <a href="#STATE_DISCONNECTED">STATE_DISCONNECTED</a>: The stream has stopped because we got disconnected from the network; you need to create a new stream on a new NetConnectionStateMachine
   * </li>
   * </ul>
   * </p>
   * <p>
   * To use you call (mostly) the same methods you'd call on a
   * flash.net.NetStream object.
   * </p>
   * <p>
   * You then register EventListers for net.theyard.components.sm.events.StateChangeEvents when you want to see changes, or just ask the object for its current state.
   * </p>
   * @see net.theyard.components.sm.events.StateChangeEvent
   * @see flash.net.NetConnection
   * @see flash.net.NetStream
   * @see net.theyard.components.netconnectionsm.NetConnectionStateMachine
   *
   */
  public class NetStreamStateMachine extends StateMachine
  {

    // Let's give names to each state
    static public const STATE_PLAYING:String = "NetStreamStateMachine.State.Playing";
    static public const STATE_STARTPLAY:String = "NetStreamStateMachine.State.StartPlay";

    static public const STATE_PUBLISHING:String = "NetStreamStateMachine.State.Publishing";
    static public const STATE_STARTPUBLISH:String = "NetStreamStateMachine.State.StartPublish";

    static public const STATE_STOPPING:String = "NetStreamStateMachine.State.Stopping";
    static public const STATE_STOPPED:String = "NetStreamStateMachine.State.Stopped";

    static public const STATE_PAUSE:String = "NetStreamStateMachine.State.Pause";

    static public const STATE_DISCONNECTED:String = "NetStreamStateMachine.State.Disconnected";

    private var ns:NetStream;

    private var ncsm:NetConnectionStateMachine;

    private var currentStreamName:String;
    private var currentPublishMode:String;

    private var _bufferTime:Number;
    private var _receiveAudio:Boolean;
    private var _receiveVideo:Boolean;
    private var _receiveVideoFPS:Number;
    private var _camera:Camera;
    private var _mic:Microphone;
    private var _snapshotMilliseconds:int;

    private var _watchingVideos:Dictionary;


    /**
     * Create a NetStreamStateMachine from a NetConnectionStateMachine
     *
     * @see NetConnectionStateMachine#getNewNetStream()
     */
    public function NetStreamStateMachine (
        ncsm: NetConnectionStateMachine)
    {
      // Set up all the states we need to support
      super();
      this.addState(new StartPlayState(this));
      this.addState(new PlayingState(this));

      this.addState(new StartPublishState(this));
      this.addState(new PublishingState(this));

      this.addState(new PauseState(this));

      this.addState(new StoppingState(this));
      this.addState(new StoppedState(this));

      this.addState(new DisconnectedState(this));

      this.setStartingState(STATE_STOPPED);

      if (ncsm == null)
      {
        throw new ArgumentError("Need a non-null NetConnectionStateMachine");
      }
      this.ncsm = ncsm;

      _bufferTime = 0;
      _receiveAudio = true;
      _receiveVideo = true;
      _receiveVideoFPS = -1;
      _camera = null;
      _mic = null;

      _watchingVideos = new Dictionary();

      this.ns = null;

      this.currentStreamName = null;
      this.currentPublishMode = null;

      // If we're working with a remote connection we want to catch the
      // disconnections to we can mark this object as DISCONNECTED if
      // needed.s
      ncsm.addEventListener(
          StateChangeEvent.STATE_CHANGE, this.ncStateChangeHandler,
          false, // default value
          0, // default value
          true); // use a weak reference to avoid the net connection SM maintaining a refer to us
      // since we maintain a refer to them.
      // And if we're already connected, create a NetStream; otherwise we need to wait
      if (ncsm.connected)
        createNetStream();
    }

    /**
     * Set the buffer time on this stream.
     *
     * @param bufferTime the buffer time in milliseconds
     *
     * @see flash.net.NetStream#bufferTime
     */
    public function setBufferTime(bufferTime:Number):void
    {
      _bufferTime = bufferTime;
      if (ns != null)
      {
        ns.bufferTime = _bufferTime;
      }
    }

    /**
     * This special function tells NetStream to stop, disconnect from the
     * NetConnectionStateMachine and release any references.
     */
    public function destroy():void
    {
      ncsm.removeEventListener(
          StateChangeEvent.STATE_CHANGE, this.ncStateChangeHandler,
          false);
      invalidateNetStream();
    }

    /**
     * Ask the state machine to start playing
     *
     * @see flash.net.NetStream#play()
     */
    public function play(name:Object, ...args):void
    {
      // package the args for the deferrer
      args.unshift(name);
      args.unshift(ns);
      args.unshift(state);
      args.unshift(StreamState(state).play);
      this.deferCallUntilStableState.apply(this, args);
      //deferCallUntilStableState(StreamState(state).play, state, ns, flv);
    }

    /**
     * Ask the state machine to start publishing
     *
     * @see flash.net.NetStream#publish()
     */
    public function publish(flv:String, publishType:String):void
    {
      deferCallUntilStableState(
          StreamState(state).publish, state, ns, flv, publishType);
    }

    /**
     * Ask the state machine to stop playing (if playing) or publishing (if publishing)
     *
     * @see flash.net.NetStream#stop()
     */
    public function stop():void
    {
      deferCallUntilStableState(StreamState(state).stop, state, ns);
    }

    /**
     * Toggle pause if playing; ignore if not playing.
     *
     * @see flash.net.NetStream#togglePause()
     */
    public function togglePause():void
    {
      deferCallUntilStableState(StreamState(state).togglePause, state, ns);
    }

    /**
     * Set receiveAudio
     *
     * @see flash.net.NetStream#receiveAudio()
     */
    public function receiveAudio(flag:Boolean):void
    {
      Utils.ytrace("changing receiveAudio to: " + flag);
      _receiveAudio = flag;
      if (ns != null)
      {
        ns.receiveAudio(_receiveAudio);
      }
    }

    /**
     * Set receiveVideo
     *
     * @see flash.net.NetStream#receiveVideo()
     */
    public function receiveVideo(flag:Boolean):void
    {
      Utils.ytrace("changing receiveVideo to: " + flag);
      _receiveVideo = flag;
      if (ns != null)
      {
        ns.receiveVideo(_receiveVideo);
      }
    }
    /**
     * Set receiveVideoFPS
     *
     * @see flash.net.NetStream#receiveVideoFPS()
     */
    public function receiveVideoFPS(fps:Number):void
    {
      Utils.ytrace("changing receiveVideoFPS to: " + fps);
      _receiveVideoFPS = fps;
      if (ns != null && _receiveVideoFPS >= 0)
      {
        ns.receiveVideoFPS(_receiveVideoFPS);
      }
    }
    /**
     * Attach a microphone to this netstream
     *
     * @see flash.net.NetStream#attachAudio()
     */
    public function attachAudio(microphone:Microphone):void
    {
      Utils.ytrace("attachAudio: " + (microphone != null ? microphone.name : "null"));
      _mic = microphone;
      if (this.ns != null)
      {
        this.ns.attachAudio(_mic);
      }
    }
    /**
     * Attach a camera to this netstream
     *
     * @see flash.net.NetStream#attachCamera()
     */
    public function attachVideo(camera:Camera, snapshotMilliseconds:int = -1):void
    {
      Utils.ytrace("attachVideo: " + (camera != null ? camera.name : "null"));
      _camera = camera;
      _snapshotMilliseconds = snapshotMilliseconds;
      if (this.ns != null)
      {
        this.ns.attachCamera(_camera, _snapshotMilliseconds);
      }
    }

    /**
     * Attaches this netstream to a video object.
     *
     * NetStream state machines track ALL video objects they are attached to,
     * and invalidates all if a disconnect occurrs.
     *
     * @see flash.media.Video.attachNetStream()
     */
    public function attachToVideo(video:Video):void
    {
      Utils.ytrace("attaching this netstream to video");
      if (video == null)
      {
        throw new ArgumentError("Video cannot be null; use detachFromVideo if you want to detach");
      }
      _watchingVideos[video] = video;
      video.attachNetStream(this.ns);
    }
    /**
     * Detaches this netstream from a video object.
     *
     * NetStream state machines track ALL video objects they are attached to,
     * and invalidates all if a disconnect occurrs.
     *
     * @see flash.media.Video.attachNetStream()
     */
    public function detachFromVideo(video:Video):void
    {
      video.attachNetStream(null);
      try
      {
        delete _watchingVideos[video];
      }
      catch (ex:Error)
      {
        //ignore any exceptions
      }
    }

    /**
     * Get the buffer length.
     *
     * @see flash.net.NetStream#bufferLength
     */
    public function get bufferLength(): Number
    {
      return ns == null ? 0 : ns.bufferLength;
    }

    /**
     * Get the buffer time.
     *
     * @see flash.net.NetStream#bufferTime
     */
    public function get bufferTime():Number
    {
      return ns == null ? 0 : ns.bufferTime;
    }

    /**
     * Get the bytes loaded
     *
     * @see flash.net.NetStream#bytesLoaded
     */
    public function get bytesLoaded():uint
    {
      return ns == null ? 0 : ns.bytesLoaded;
    }

    /**
     * Get the bytes total
     *
     * @see flash.net.NetStream#bytesTotal
     */
    public function get bytesTotal():uint
    {
      return ns == null ? 0 : ns.bytesTotal;
    }

    /**
     * Get the live delay
     *
     * @see flash.net.NetStream#liveDelay
     */
    public function get liveDelay():Number
    {
      return ns == null ? 0 : ns.liveDelay;
    }

    /**
     * Get the stream time
     *
     * @see flash.net.NetStream#time
     */
    public function get time():Number
    {
      return ns == null ? 0 : ns.time;
    }

    /**
     * Get the current frames per second
     *
     * @see flash.net.NetStream#currentFPS
     */
    public function get currentFPS():Number
    {
      return ns == null ? 0 : ns.currentFPS;
    }

    /**
     * Get a sound transform for current audio
     *
     * @see flash.net.NetStream#soundTransform
     */
    public function get soundTransform():SoundTransform
    {
      return ns == null ? null : ns.soundTransform;
    }
    /**
     * Get the current stream name.
     * @return Returns the current stream name if publishing or playing, null otherwise.
     */
    public function getCurrentStreamName():String
    {
      return this.currentStreamName;
    }

    /**
     * Get the current publishing mode.
     * @return Returns the current publish mode if publishing, null otherwise.
     */
    public function getCurrentPublishMode():String
    {
      return this.currentPublishMode;
    }

    /**
     * What we use to get events from a NetStream
     *
     * If you want to you can override this method, but always call it back
     * if you want things to still work.
     */
    protected function netStatusHandler(event:NetStatusEvent):void
    {
      var code:String = event.info.code;
      Utils.ytrace(Utils.yAssocArrayAsString(event.info));

      // Use to handle all the playback function stuff.
      // Note that all netStatus callbacks need to be re-entrant (i.e.
      // you cannot defer them until the gotoState()s are done.
      switch (code)
      {
        case NetStatus.STREAM_PLAY_UNPUBLISHNOTIFY:
        case NetStatus.STREAM_PLAY_FAILED:
        case NetStatus.STREAM_PLAY_STOP:
        case NetStatus.STREAM_PLAY_STREAMNOTFOUND:
          Utils.ytrace("got event that stops stream: " + this.currentStreamName);
          StreamState(state).onPlayStop(ns);
          break;

        case NetStatus.STREAM_PUBLISH_BADNAME:
        case NetStatus.STREAM_RECORD_FAILED:
        case NetStatus.STREAM_RECORD_NOACCESS:
        case NetStatus.STREAM_UNPUBLISH_SUCCESS:
          Utils.ytrace("got event that stops stream: " + this.currentStreamName);
          StreamState(state).onPublishStop(ns);
          break;


        case NetStatus.STREAM_PLAY_RESET:
          Utils.ytrace("got a stream reset. ignoring.");
          break;
        case NetStatus.STREAM_PUBLISH_START:
          StreamState(state).onPublishStart(ns);
          break;

        case NetStatus.STREAM_PLAY_START:
          StreamState(state).onPlayStart(ns);
          break;

        case NetStatus.STREAM_PLAY_PUBLISHNOTIFY:
        case NetStatus.STREAM_PUBLISH_IDLE:
        case NetStatus.STREAM_BUFFER_EMPTY:
        case NetStatus.STREAM_BUFFER_FLUSH:
        case NetStatus.STREAM_BUFFER_FULL:
        case NetStatus.STREAM_PAUSE_NOTIFY:
        case NetStatus.STREAM_SEEK_FAILED:
        case NetStatus.STREAM_SEEK_INVALIDTIME:
        case NetStatus.STREAM_SEEK_NOTIFY:
          // RECORD events are interesting, but we care more
          // about PUBLISH events.
        case NetStatus.STREAM_RECORD_START:
        case NetStatus.STREAM_RECORD_STOP:
          // ignore
          Utils.ytrace("Ignore stream NetStatus event: " + event.info.code);
          break;
        default:
          // ignore any other items; we'll not track for now.
          //Utils.ytrace("Got unexpected stream NetStatus event: " + event.info.code);
          break;
      }
    }

    /**
     * What we use to get events from a NetStream
     *
     * If you want to you can override this method, but always call it back
     * if you want things to still work.
     */
    protected function ncStateChangeHandler(event: StateChangeEvent ) : void
    {
      switch (event.getState())
      {
        case NetConnectionStateMachine.STATE_DISCONNECTING:
          // We close everthing when we get the DISCONNECTING event;
          // that way we clean up before the NetConnectionStateMachine
          // actually closes the connection.
          StreamState(state).onDisconnect(ns);
          break;
        case NetConnectionStateMachine.STATE_CONNECTED:
          // When we connect we need to create or recreate
          // all underlying netstreams.
          // This has the nice side effect of anytime the NCSM
          // that this is built on re-connects, we update all
          // our NetStreams behind the scenes and notify all
          // clients appropriately.
          createNetStream();
          break;
        default:
          // ignore.
          break;
      }
    }

    /**
     * Called by Flash -- DO NOT CALL DIRECTLY
     */
    public function onPlayStatus( info : Object ) : void
    {
      Utils.ytrace(Utils.yAssocArrayAsString(info));
      if (info.code == "NetStream.Play.Complete")
      {
        StreamState(state).stop(ns);
      }
    }
    /**
     * Called by Flash -- DO NOT CALL DIRECTLY
     */
    public function onTextData( info : Object) : void
    {
      Utils.ytrace(Utils.yAssocArrayAsString(info));
    }
    /**
     * Called by Flash -- DO NOT CALL DIRECTLY
     */
    public function onImageData( info : Object) : void
    {
      Utils.ytrace(Utils.yAssocArrayAsString(info));
    }
    /**
     * Called by Flash -- DO NOT CALL DIRECTLY
     */
    public function onMetaData ( info : Object ) : void
    {
      Utils.ytrace(Utils.yAssocArrayAsString(info));
      this.dispatchEvent(new MetaDataEvent(info));
    }
    /**
     * Called by Flash -- DO NOT CALL DIRECTLY
     */
    public function onCuePoint( info : Object ) : void
    {
      Utils.ytrace(Utils.yAssocArrayAsString(info));
      this.dispatchEvent(new CuePointEvent(info));
    }


    /**
     * What we use to get IOErrorEvents from a NetStream
     *
     * If you want to you can override this method, but always call it back
     * if you want things to still work.
     */
    protected function netIOError(event:IOErrorEvent):void
    {
      Utils.ytrace("io error: " + event);
    }

    /**
     * What we use to get AsyncErrorEvents from a NetStream
     *
     * If you want to you can override this method, but always call it back
     * if you want things to still work.
     */
    protected function netASyncError(event:AsyncErrorEvent):void
    {
      Utils.ytrace("async error: " + event.error);
    }

    internal function setStreamInfo(flv:String, publishType:String) : void
    {
      currentStreamName = flv;
      currentPublishMode = publishType;
    }

    internal function invalidateNetStream() : void
    {
      if (ns != null)
      {
        // First stop
        this.stop();
        // Now, deregister event handlers.
        ns.removeEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
        ns.removeEventListener(IOErrorEvent.IO_ERROR, netIOError);
        ns.removeEventListener(AsyncErrorEvent.ASYNC_ERROR, netASyncError);

        // now detach from all videos.
        for (var o:Object in _watchingVideos)
        {
          var vid:Video = Video(o);
          vid.attachNetStream(null);
        }

        // empty out stream name parameters
        this.currentStreamName = null;
        this.currentPublishMode = null;

        // and clear out the net stream
        ns = null;
      }
    }
    internal function createNetStream() : void
    {
      if (ns != null)
      {
        this.invalidateNetStream();
      }
      var nc :NetConnection = null;
      if (ncsm.connected)
        nc = ncsm.nc;

      ns = new NetStream(nc);
      ns.client = this;

      // Reset the same paremeters as the last NetStream had.
      this.setBufferTime(_bufferTime);
      this.receiveAudio(_receiveAudio);
      this.receiveVideo(_receiveVideo);
      this.receiveVideoFPS(_receiveVideoFPS);
      if (_camera != null)
      {
        this.attachVideo(_camera, _snapshotMilliseconds);
      }
      if (_mic != null)
      {
        this.attachAudio(_mic);
      }

      this.currentStreamName = null;
      this.currentPublishMode = null;

      ns.addEventListener(NetStatusEvent.NET_STATUS, this.netStatusHandler);
      ns.addEventListener(IOErrorEvent.IO_ERROR, netIOError );
      ns.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netASyncError );

      // now, reset our state machine, sending an event if necessary.
      this.gotoState(STATE_STOPPED);

      // and reattach our videos
      for (var o:Object in _watchingVideos)
      {
        var vid:Video = Video(o);
        vid.attachNetStream(ns);
      }
      // and fire the onReconnect() back to the state.
      StreamState(state).onReconnect();
    }

    /**
     * Convenience method for adding an event listener directly to the underlying NetStream.
     *
     * @see flash.net.NetStream.addEventListener()
     */
    public function addNsListener(
        type:String, listener:Function, useCapture:Boolean = false, 
        priority:int = 0, useWeakReference:Boolean = false):void
    {
      ns.addEventListener(
          type, listener, useCapture, priority, useWeakReference);
    }

    /**
     * Convenience method for removing an event listener directly from the underlying NetStream.
     *
     * @see flash.net.NetStream.removeEventListener()
     */
    public function removeNsListener(
        type:String, listener:Function, useCapture:Boolean = false):void
    {
      ns.removeEventListener(type, listener, useCapture);
    }
  }
}
